package de.upb.aquarium;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLException;
import javax.media.opengl.glu.GLU;
import javax.naming.InsufficientResourcesException;

import com.sun.opengl.util.GLUT;

import de.upb.aquarium.api.camera.Camera;
import de.upb.aquarium.api.camera.CameraAvatar;
import de.upb.aquarium.api.camera.PerspectiveProjection;
import de.upb.aquarium.api.light.Light;
import de.upb.aquarium.api.light.PointLightSource;
import de.upb.aquarium.api.light.SpotlightSource;
import de.upb.aquarium.api.objects.ComposedObject;
import de.upb.aquarium.api.objects.IPaintable;
import de.upb.aquarium.api.objects.StaticObject;
import de.upb.aquarium.api.objects.TransparentObject;
import de.upb.aquarium.api.utility.VectorDouble;
import de.upb.aquarium.api.utility.VectorFloat;
import de.upb.aquarium.user.CG2InputListener;
import de.upb.aquarium.water.Surface;
import de.upb.aquarium.water.WaveGenerator;
import de.upb.aquarium.water.bubbles.BubbleManager;
import de.upb.aquarium.water.bubbles.BubbleSource;
import de.upb.aquarium.water.bubbles.PointBubbleSource;
import de.upb.aquarium.water.pump.WaterParticle;
import de.upb.aquarium.water.pump.WaterParticleManager;
import de.upb.aquarium.water.pump.WaterPump;

/**
 * This class controls the main application behavior. It is used for setting
 * global parameters, getting user input and such things.
 * This class exists as a singleton, the only instance can be retrieved by
 * <code>World.getInstance()</code>.
 * @author Jost
 */
public class World implements GLEventListener {

	/**
	 * The instance.
	 */
	private static World instance = null;

	/**
	 * Gets the singleton instance.
	 * @return The singleton instance.
	 */
	public static World getInstance()
	{
		if (instance == null)
		{
			instance = new World();
		}
		return instance;
	}

	/**
	 * This constructor is private, so no one may create an "unauthorized"
	 * instance of this class. All initialization of this class should happen
	 * here.
	 */
	private World()
	{
		this.glu = new GLU();
		this.glut = new GLUT();

		this.objects = new ComposedObject();
		this.objects.setAbsolutePosition(new float[] {0,0,0});
		this.transparentObjects = new ArrayList<TransparentObject>();

		projection = new PerspectiveProjection(4.0/3.0, 30.0, 50.0, 1.0);

		VectorDouble position	= new VectorDouble(new double[] {5.0, 6.0, 5.0});
		VectorDouble at			= new VectorDouble(new double[] {5.0, 6.0, 0.0});
		VectorDouble up			= new VectorDouble(new double[] {0.0, 1.0, 0.0});

		camera = new Camera(projection,
				position,
				at,
				up);

		inputListener = new CG2InputListener(
				position,			// Initial position
				at.sub(position),	// Initial direction
				20.0,				// Movement speed
				20.0);				// Rotation speed

		lights = new ArrayList<Light>();
		lights.add(new SpotlightSource(
				new VectorFloat(new float[] {4.5f, 5.0f, -5.0f}),
				new VectorFloat(new float[] {1.0f, 0.0f, -1.0f}),
				1.0f,
				0.0f,
				0.0f,
				5.0f,
				60.0f,
				new VectorFloat(new float[] {0.0f, 0.0f, 1.0f, 0.0f}),
				new VectorFloat(new float[] {0.0f, 1.0f, 0.0f, 0.0f}),
				new VectorFloat(new float[] {0.0f, 0.0f, 1.0f, 0.0f})));
		lights.add(new PointLightSource(
				new VectorFloat(new float[]{4.25f, 5.0f, -3.4f}),
				1.0f, 1.0f, 0.0f,
				new VectorFloat(new float[]{0.0f, 0.0f, 0.0f, 0.0f}),
				new VectorFloat(new float[]{1.0f, 1.0f, 1.0f, 0.0f}),
				new VectorFloat(new float[]{1.0f, 1.0f, 1.0f, 0.0f}))
		);
	}

	///////////////////////////////////////////////////////////////////////////
	// Members                                                               //
	///////////////////////////////////////////////////////////////////////////

	private GL gl;
	private final GLU glu;
	private final GLUT glut;

	private final PerspectiveProjection projection;
	private final Camera camera;
	private final CG2InputListener inputListener;
	ComposedObject objects;
	private final List<TransparentObject> transparentObjects;
	private final ArrayList<Light> lights;
	private CameraAvatar cameraAvatarNormal;
	
	private Surface surface;
	private WaveGenerator waveGen;
	private WaterPump pump;
	
	private boolean initializationDone = false;
	private boolean drawDiagnosisInfo = false;


	///////////////////////////////////////////////////////////////////////////
	// Content Management                                                    //
	///////////////////////////////////////////////////////////////////////////

	/**
	 * Create the scene here, i.e. load and add objects here.
	 * @throws IOException
	 * @throws GLException
	 */
	private void createScene() throws GLException, IOException
	{

		System.out.println("Loading started...");

		ComposedObject aquarium = new Aquarium(gl);

		StaticObject room = new StaticObject(gl, "models/room.obj", 1, true);
		StaticObject pump = new StaticObject(gl, "models/pump2.obj", 1f, false);
		pump.setRotation(180f, -90f, -90f);
		pump.setPosition(5.6f, 4.2f, -9.1f);
		
		addPaintable(room);
		addPaintable(pump);
		addPaintable(aquarium);

		setUpWater();
		
		cameraAvatarNormal = new CameraAvatar();
		addPaintable(cameraAvatarNormal);

		System.out.println("Loading finished.");
	}
	
	private void setUpWater() throws IOException
	{		
		
		double surfaceWidth = 2.7;
		double surfaceDepth = 4.825;
		
		int horizontalResolution = 50;
		int verticalResolution = 50;
		
		int waveTimeResolution = 1000;
		double waveRotationTime = 0.5;
		
		waveGen = new WaveGenerator(
				horizontalResolution, verticalResolution,
				surfaceWidth, surfaceDepth,
				waveTimeResolution, waveRotationTime);
		
		WaterParticleManager.getInstance().setWaveGenerator(waveGen);
		
		surface = new Surface(
				surfaceWidth, surfaceDepth,
				horizontalResolution, verticalResolution,
				waveGen);
		surface.setPosition(new float[] {4.25f, 4.8f, -3.4f});
		surface.setRotation(new float[] {-90.0f, 0.0f, 0.0f});

		// bubbles are under water, so paint them first. Water particles
		// are above water, so paint them after the surface.
		addPaintable(BubbleManager.getInstance());
		addPaintable(surface);
		addPaintable(WaterParticleManager.getInstance());
		
		BubbleSource bubbleSource1 = new PointBubbleSource(
				new VectorFloat(new float[] {5.0f, 3.6f, -5.0f}),	// position
				surface.getSurfaceNormal().mul(0.01f),				// up vector
				20.0,												// spawn ratio
				0.5,												// max speed
				0.1);												// max deviation
		
		BubbleSource bubbleSource2 = new PointBubbleSource(
				new VectorFloat(new float[] {5.5f, 3.6f, -7.2f}),	// position
				surface.getSurfaceNormal().mul(0.01f),				// up vector
				2.0,												// spawn ratio
				0.5,												// max speed
				0.1);												// max deviation

		BubbleManager.getInstance().setSurfacePlane(0.0, 1.0, 0.0, -4.75);
		BubbleManager.getInstance().addBubbleSource(bubbleSource1);
		BubbleManager.getInstance().addBubbleSource(bubbleSource2);
		
		pump = new WaterPump(
				new VectorDouble(new double[] {5.55, 5.9, -8.0}),
				new VectorDouble(new double[] {5.55, 4.9, -7.0}),
				new VectorDouble(new double[] {0.0, -1.0, 0.0}),
				0.1,
				1.0,
				200);

		WaterParticleManager.getInstance().setSurfacePosition(new VectorFloat(surface.getPosition()));
		WaterParticleManager.getInstance().setSurfacePlane(0.0, 1.0, 0.0, -4.75);
		WaterParticleManager.getInstance().addPump(pump);
		
		WaterParticle.lifeTime = 0.5;
	}

	///////////////////////////////////////////////////////////////////////////
	// GLEventListener methods                                               //
	///////////////////////////////////////////////////////////////////////////

	@Override
	public void display(GLAutoDrawable drawable) {

		gl = drawable.getGL();
		beforeFrame();

		// First render pass (for reflection):
		VectorDouble oldCameraPosition = camera.getPosition();
		VectorDouble oldCameraDirection = camera.getDirection();

		VectorDouble reflectionCamPos = surface.computeReflectionCameraPosition(oldCameraPosition);
		VectorDouble reflectionCamDir = surface.computeReflectionCameraDirection(oldCameraDirection);

		camera.setPosition(reflectionCamPos);
		camera.setDirection(reflectionCamDir);

		// Remove surface from render queue and set clipping plane to render
		// only potentially reflected things:
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glLoadIdentity();
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();

		camera.prepareRendering();
		gl.glEnable(GL.GL_CLIP_PLANE0);
		surface.setClippingPlane(GL.GL_CLIP_PLANE0);
		surface.setRender(false);

		render(gl);

		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glLoadIdentity();
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();
		camera.prepareRendering();
		surface.grabReflectionMap();

		// And reestablish the current render state.
		gl.glDisable(GL.GL_CLIP_PLANE0);
		surface.setRender(true);

		// Reset camera to normal position, direction and opening angle:
		camera.setPosition(oldCameraPosition);
		camera.setDirection(oldCameraDirection);

		// Second render pass (the one which is shown):
		render(gl);

		if (drawDiagnosisInfo)
		{
			drawDiagnosisInfo(-0.975, 0.95, 0.0);
		}
		
		afterFrame();
		gl = null;
	}

	public void render(GL gl)
	{
		beforeRendering();

		transparentObjects.clear();

		// Only paints opaque objects, the transparent ones are stored in transparentObjects
		objects.paint(gl);

		// Sort transparent objects
		Collections.sort(transparentObjects);

		for (TransparentObject o : transparentObjects)
		{
			o.paint(gl);
		}

		afterRendering();
	}

	@Override
	public void displayChanged(GLAutoDrawable drawable, boolean modeChanged,
			boolean deviceChanged) {
		gl = drawable.getGL();
		// Not implemented (the docs say we don't need to...)
		gl = null;
	}

	@Override
	public void init(GLAutoDrawable drawable) {
		gl = drawable.getGL();

		if (!initializationDone) {
			// Add listeners
			drawable.addKeyListener(inputListener);
			drawable.addMouseMotionListener(inputListener);
			drawable.addMouseListener(inputListener);

			// Do that GL enabling and disabling stuff:
			gl.glEnable(GL.GL_DEPTH_TEST);
			gl.glEnable(GL.GL_LIGHTING); // Particular lights are enabled and
			// disabled by their classes.

			gl.glEnable(GL.GL_LIGHT_MODEL_AMBIENT);
			gl.glLightModelfv(GL.GL_LIGHT_MODEL_AMBIENT,
					new float[] {0.2f, 0.2f, 0.2f, 1.0f}, 0);
			gl.glEnable(GL.GL_TEXTURE_2D);
			gl.glEnable(GL.GL_COLOR_MATERIAL);
			gl.glColorMaterial(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE);

			// Create scene
			try {
				createScene();
			} catch (IOException e) {
				e.printStackTrace();
				System.exit(1);
			}

			initializationDone = true;
		}

		gl = null;
	}

	@Override
	public void reshape(GLAutoDrawable drawable, int x, int y, int width,
			int height) {
		gl = drawable.getGL();

		projection.setAspectRatio(((double)width)/((double)height));

		gl = null;
	}

	///////////////////////////////////////////////////////////////////////////
	// Methods initializing and finalizing                                   //
	///////////////////////////////////////////////////////////////////////////

	/**
	 * Called before rendering. Do any render pass specific set up,
	 * e.g. reset buffers etc., here.
	 */
	private void beforeRendering() {

		// Reset the matrices to the identity.
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();

		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glLoadIdentity();

		// Reset the clear color.
		gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

		// Clear the buffers.
		gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT | GL.GL_STENCIL_BUFFER_BIT);

		camera.prepareRendering();

		enableLights();
	}

	/**
	 * Do any needed post rendering cleanup here.
	 */
	private void afterRendering() {

	}

	/**
	 * Initialize frame. Do any frame specific setup here, e.g.
	 * move the camera, animate objects etc.
	 */
	private void beforeFrame()
	{
		// Update camera
		camera.setPosition(inputListener.getPosition());
		camera.setDirection(inputListener.getDirection());
		
		if (inputListener.getKeyTyped('b'))
		{
			pump.setOn(!pump.isOn());
		}
		
		if (inputListener.getKeyTyped('c'))
		{
			surface.setRenderControlPoints(!surface.isRenderControlPoints());
			WaterParticle.setRenderControlPoints(!WaterParticle.isRenderControlPoints());
		}
		
		if (inputListener.getKeyTyped('v'))
		{
			drawDiagnosisInfo = !drawDiagnosisInfo;
		}
		
		cameraAvatarNormal.setPosition(new VectorFloat(camera.getPosition().data).data);
		cameraAvatarNormal.setDirection(new VectorFloat(camera.getDirection().data).data);
		
		updateFPS();
	}

	/**
	 * Do per-frame-cleanups here.
	 */
	private void afterFrame()
	{
		// nothing to do.
	}

	/**
	 * Enable the lights here, and make sure they are positioned in the
	 * world coordinate system.
	 */
	private void enableLights()
	{
		try
		{
			for (Light light : lights)
			{
				light.setOn(true);
			}
		}
		catch (InsufficientResourcesException e)
		{
			System.out.println("To many lights in scene.");
		}
	}

	////////////////bit///////////////////////////////////////////////////////////
	// Adding and removing paintables                                        //
	///////////////////////////////////////////////////////////////////////////

	public void addPaintable(IPaintable paintable)
	{
		objects.add(paintable);
	}

	public void removePaintable(IPaintable paintable)
	{
		objects.getContainedObjects().remove(paintable);
	}

	public void clearPaintables()
	{
		objects.getContainedObjects().clear();
	}

	///////////////////////////////////////////////////////////////////////////
	// Methods for retrieving globals                                        //
	////////////////bit///////////////////////////////////////////////////////////

	public List<Light> getLights()
	{
		return lights;
	}
	
	public VectorDouble getCameraPosition()
	{
		return camera.getPosition();
	}

	/**
	 * Returns the global time as a double in seconds.
	 * @return Global time in seconds as a double. Always positive.
	 */
	public double getGlobalTime()
	{
		return (System.currentTimeMillis())/1000.0;
	}

	/**
	 * Convenience method for retrieving the elapsed time since a given
	 * time.
	 * @param since Time from which to count.
	 * @return Time elapsed since <code>since</code> in seconds. Negative, if
	 * 		since is in the future, nonnegative otherwise.
	 */
	public double getElapsedTime(double since)
	{
		return getGlobalTime() - since;
	}

	/**
	 * Returns the current GL object. If there is no current GL object, this
	 * method may return <code>null</code>.
	 * It is probably a good idea to <i>not</i> check the return value, since
	 * when you get a null from this method, something is really wrong.
	 */
	public GL getGL()
	{
		return gl;
	}

	/**
	 * @return The global GLU object.
	 */
	public GLU getGLU() {
		return glu;
	}

	/**
	 * @return The global GLUT object.
	 */
	public GLUT getGLUT() {
		return glut;
	}

	public List<TransparentObject> getTransparentObjects() {
		return transparentObjects;
	}

	public Camera getCamera() {
		return camera;
	}

	///////////////////////////////////////////////////////////////////////////
	// Methods for diagnosis                                                 //
	///////////////////////////////////////////////////////////////////////////

	private int frameCount = 0;
	private double fps = 0;
	private double startTime = -1.0;

	private void updateFPS()
	{
		frameCount++;
		if (startTime < 0.0)
		{
			startTime = World.getInstance().getGlobalTime();
		}
		
		if (World.getInstance().getElapsedTime(startTime) >= 1.0)
		{
			fps = frameCount/World.getInstance().getElapsedTime(startTime);
			frameCount = 0;
			startTime += 1.0;
		}
	}
	
	/**
	 * Draws diagnosis information at the given coordinates. These
	 * coordinates are normalized device coordinates: The middle
	 * of the view volume has coordinates (0,0,0), the upper far left (-1,1,1),
	 * the lower near right (1,-1, -1).
	 * @param x
	 * @param y
	 * @param z
	 */
	private void drawDiagnosisInfo(double x, double y, double z)
	{
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glPushMatrix();
		gl.glLoadIdentity();
		
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glPushMatrix();
		gl.glLoadIdentity();

		int[] viewport = new int[4];
		gl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);
		double pixelheight = 2.0/(viewport[2] - viewport[0]);
		double pixelwidth  = 2.0/(viewport[3] - viewport[1]);
		
		gl.glRasterPos3d(x, y, z);
		glut.glutBitmapString(GLUT.BITMAP_TIMES_ROMAN_10, "FPS: " + fps);
		
		gl.glRasterPos3d(x, y-16*pixelheight, z);
		glut.glutBitmapString(GLUT.BITMAP_TIMES_ROMAN_10, "Number of bubbles: " + BubbleManager.getInstance().getNumBubbles());
		
		gl.glRasterPos3d(x, y-32*pixelheight, z);
		glut.glutBitmapString(GLUT.BITMAP_TIMES_ROMAN_10, "Number of water particles: " + WaterParticleManager.getInstance().getNumParticles());
		
		surface.drawDiagnosisInfo(x, y-48*pixelheight, z, 0.4, 0.4, 0.02);

		
		
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glPopMatrix();

		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glPopMatrix();
	}
}
